/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2002 - 2007 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated
 * and its suppliers and may be covered by U.S. and Foreign Patents,
 * patents in process, and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package flex.messaging.io.amf;

import flex.messaging.MessageException;
import flex.messaging.io.ArrayCollection;
import flex.messaging.io.PagedRowSet;
import flex.messaging.io.PropertyProxy;
import flex.messaging.io.PropertyProxyRegistry;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.SerializationDescriptor;
import flex.messaging.io.StatusInfoProxy;
import flex.messaging.io.BeanProxy;
import flex.messaging.util.Trace;
import hhf.flex.annotations.entities.FlexTransient;
import java.lang.reflect.InvocationTargetException;
import org.w3c.dom.Document;

import java.io.IOException;
import java.io.Externalizable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.sql.RowSet;

/**
 * Serializes data to an output stream using the new
 * AMF 3 format.
 * <p>
 * This class intends to match the Flash Player 8 C++ code
 * in avmglue/DataIO.cpp
 * </p>
 *
 * @author Peter Farland
 * @exclude
 */
public class Amf3Output extends AbstractAmfOutput implements Amf3Types {

	/**
	 * Permet de déterminer si un attribut estlazyloading
	 * @param o
	 * @return 
	 */
	private boolean isInstantiated(Object o) {
		// teste pour toplink et eclipselink
		try {
			Method getValueHolder = o.getClass().getMethod("getValueHolder");
			Object valueHolder = getValueHolder.invoke(o);
			Method isInstantiated = valueHolder.getClass().getMethod("isInstantiated");
			return (Boolean) isInstantiated.invoke(valueHolder);
		} catch (IllegalAccessException ex) {
		} catch (IllegalArgumentException ex) {
		} catch (InvocationTargetException ex) {
		} catch (NoSuchMethodException ex) {
		} catch (SecurityException ex) {
		}
		return true;
	}

	/**
	 * @exclude
	 */
	protected IdentityHashMap<Object, Integer> objectTable;
	/**
	 * @exclude
	 */
	protected HashMap<TraitsInfo, Integer> traitsTable;
	/**
	 * @exclude
	 */
	protected HashMap<String, Integer> stringTable;

	public Amf3Output(SerializationContext context) {
		super(context);
		context.supportDatesByReference = true;
	}

	@Override
	public void reset() {
		super.reset();
		if (objectTable != null) {
			objectTable.clear();
		}
		if (traitsTable != null) {
			traitsTable.clear();
		}
		if (stringTable != null) {
			stringTable.clear();
		}
	}

	//
	// java.io.ObjectOutput IMPLEMENTATIONS
	//
	/**
	 * Serialize an Object using AMF 3.
	 */
	@Override
	public void writeObject(Object o) throws IOException {
		if (o == null) {
			writeAMFNull();
			return;
		}

		if (!context.legacyExternalizable && o instanceof Externalizable) {
			writeCustomObject(o);
		} else if (o instanceof String || o instanceof Character) {
			String s = o.toString();
			writeAMFString(s);
		} else if (o instanceof Number) {
			if (o instanceof Integer || o instanceof Short || o instanceof Byte) {
				int i = ((Number) o).intValue();
				writeAMFInt(i);
			} else if (!context.legacyBigNumbers
					  && (o instanceof BigInteger || o instanceof BigDecimal)) {
				// Using double to write big numbers such as BigInteger or
				// BigDecimal can result in information loss so we write
				// them as String by default...
				writeAMFString(((Number) o).toString());
			} else {
				double d = ((Number) o).doubleValue();
				writeAMFDouble(d);
			}
		} else if (o instanceof Boolean) {
			writeAMFBoolean(((Boolean) o).booleanValue());
		} // We have a complex type...
		else if (o instanceof Date) {
			writeAMFDate((Date) o);
		} else if (o instanceof Calendar) {
			writeAMFDate(((Calendar) o).getTime());
		} else if (o instanceof Document) {
			if (context.legacyXMLDocument) {
				out.write(kXMLType); // Legacy flash.xml.XMLDocument Type
			} else {
				out.write(kAvmPlusXmlType); // New E4X XML Type
			}
			if (!byReference(o)) {
				String xml = documentToString(o);
				if (isDebug) {
					trace.write(xml);
				}

				writeAMFUTF(xml);
			}
		} // If there is a proxy for this,write it as a custom object so the default
		// behavior can be overriden.
		else if (o instanceof Enum && PropertyProxyRegistry.getRegistry().getProxy(o.getClass()) == null) {
			Enum<?> enumValue = (Enum<?>) o;
			writeAMFString(enumValue.name());
		} else {
			// We have an Object or Array type...
			Class cls = o.getClass();

			if (context.legacyMap && o instanceof Map && !(o instanceof ASObject)) {
				writeMapAsECMAArray((Map) o);
			} else if (o instanceof Collection) {
				if (context.legacyCollection) {
					writeCollection((Collection) o, null);
				} else {
					writeArrayCollection((Collection) o, null);
				}
			} else if (cls.isArray()) {
				writeAMFArray(o, cls.getComponentType());
			} else {
				//Special Case: wrap RowSet in PageableRowSet for Serialization
				if (o instanceof RowSet) {
					o = new PagedRowSet((RowSet) o, Integer.MAX_VALUE, false);
				} else if (context.legacyThrowable && o instanceof Throwable) {
					o = new StatusInfoProxy((Throwable) o);
				}

				writeCustomObject(o);
			}
		}
	}

	@Override
	public void writeObjectTraits(TraitsInfo ti) throws IOException {
		String className = ti.getClassName();

		if (isDebug) {
			if (ti.isExternalizable()) {
				trace.startExternalizableObject(className, getObjectTableSize());
			} else {
				trace.startAMFObject(className, getObjectTableSize());
			}
		}

		if (!byReference(ti)) {
			int count = 0;
			List propertyNames = null;
			boolean externalizable = ti.isExternalizable();

			if (!externalizable) {
				propertyNames = ti.getProperties();
				if (propertyNames != null) {
					count = propertyNames.size();
				}
			}

			boolean dynamic = ti.isDynamic();

			writeUInt29(3 | (externalizable ? 4 : 0) | (dynamic ? 8 : 0) | (count << 4));
			writeStringWithoutType(className);

			if (!externalizable && propertyNames != null) {
				for (int i = 0; i < count; i++) {
					String propName = (String) ti.getProperty(i);
					writeStringWithoutType(propName);
				}
			}
		}
	}

	@Override
	public void writeObjectProperty(String name, Object value) throws IOException {
		if (isDebug) {
			trace.namedElement(name);
		}
		writeObject(value);
	}

	@Override
	public void writeObjectEnd() throws IOException {
		// No action required for AMF 3

		if (isDebug) {
			trace.endAMFObject();
		}
	}

	//
	// AMF SPECIFIC SERIALIZATION IMPLEMENTATIONS
	//
	/**
	 * @exclude
	 */
	protected void writeAMFBoolean(boolean b) throws IOException {
		if (isDebug) {
			trace.write(b);
		}

		if (b) {
			out.write(kTrueType);
		} else {
			out.write(kFalseType);
		}
	}

	/**
	 * @exclude
	 */
	protected void writeAMFDate(Date d) throws IOException {
		out.write(kDateType);

		if (!byReference(d)) {
			if (isDebug) {
				trace.write(d);
			}

			//Write out an invalid reference
			writeUInt29(1);

			// Write the time as 64bit value in ms
			out.writeDouble((double) d.getTime());
		}
	}

	/**
	 * @exclude
	 */
	protected void writeAMFDouble(double d) throws IOException {
		if (isDebug) {
			trace.write(d);
		}

		out.write(kDoubleType);
		out.writeDouble(d);
	}

	/**
	 * @exclude
	 */
	protected void writeAMFInt(int i) throws IOException {
		if (i >= INT28_MIN_VALUE && i <= INT28_MAX_VALUE) {
			if (isDebug) {
				trace.write(i);
			}

			// We have to be careful when the MSB is set, as (value >> 3) will sign extend.
			// We know there are only 29-bits of precision, so truncate. This requires
			// similar care when reading an integer.
			//i = ((i >> 3) & UINT29_MASK);
			i = i & UINT29_MASK; // Mask is 2^29 - 1
			out.write(kIntegerType);
			writeUInt29(i);
		} else {
			// Promote large int to a double
			writeAMFDouble(i);
		}
	}

	/**
	 * @exclude
	 */
	protected void writeMapAsECMAArray(Map map) throws IOException {
		out.write(kArrayType);

		if (!byReference(map)) {
			if (isDebug) {
				trace.startECMAArray(getObjectTableSize());
			}

			writeUInt29((0 << 1) | 1);

			Iterator it = map.keySet().iterator();
			while (it.hasNext()) {
				Object key = it.next();
				if (key != null) {
					String propName = key.toString();
					writeStringWithoutType(propName);

					if (isDebug) {
						trace.namedElement(propName);
					}

					writeObject(map.get(key));
				}
			}

			writeStringWithoutType(EMPTY_STRING);

			if (isDebug) {
				trace.endAMFArray();
			}
		}
	}

	/**
	 * @exclude
	 */
	protected void writeAMFNull() throws IOException {
		if (isDebug) {
			trace.writeNull();
		}

		out.write(kNullType);
	}

	/**
	 * @exclude
	 */
	protected void writeAMFString(String s) throws IOException {
		out.write(kStringType);
		writeStringWithoutType(s);

		if (isDebug) {
			trace.writeString(s);
		}
	}

	/**
	 * @exclude
	 */
	protected void writeStringWithoutType(String s) throws IOException {
		if (s.length() == 0) {
			// don't create a reference for the empty string,
			// as it's represented by the one byte value 1
			// len = 0, ((len << 1) | 1).
			writeUInt29(1);
			return;
		}

		if (!byReference(s)) {
			writeAMFUTF(s);
			return;
		}
	}

	/**
	 * @exclude
	 */
	protected void writeAMFArray(Object o, Class componentType) throws IOException {
		if (componentType.isPrimitive()) {
			writePrimitiveArray(o);
		} else if (componentType.equals(Byte.class)) {
			writeAMFByteArray((Byte[]) o);
		} else if (componentType.equals(Character.class)) {
			writeCharArrayAsString((Character[]) o);
		} else {
			writeObjectArray((Object[]) o, null);
		}
	}

	/**
	 * @exclude
	 */
	protected void writeArrayCollection(Collection col, SerializationDescriptor desc) throws IOException {
		// résoud le probleme de collection lazy loading by hhf
		if(!isInstantiated(col)) {
			writeAMFNull();
			return;
		}
//		try { 
//			col.size();
//		} catch (Throwable e) {
//			writeAMFNull();
//			return;
//		}
		out.write(kObjectType);

		if (!byReference(col)) {
			ArrayCollection ac;

			if (col instanceof ArrayCollection) {
				ac = (ArrayCollection) col;
				// TODO: QUESTION: Pete, ignoring the descriptor here... not sure if
				// we should modify the user's AC as that could cause corruption?
			} else {
				// Wrap any Collection in an ArrayCollection
				ac = new ArrayCollection(col);
				if (desc != null) {
					ac.setDescriptor(desc);
				}
			}

			// Then wrap ArrayCollection in PropertyProxy for bean-like serialization
			PropertyProxy proxy = PropertyProxyRegistry.getProxy(ac);
			writePropertyProxy(proxy, ac);
		}
	}

	/**
	 * @exclude
	 */
	protected void writeCustomObject(Object o) throws IOException {
		PropertyProxy proxy = null;
		// résoud le probleme de collection lazy loading pour les maps, et autre collections by hhf
		if(!isInstantiated(o)) {
			writeAMFNull();
			return;
		}
//		try {
////			L'object à t'il une method size
//			Method getSize = o.getClass().getMethod("size");
//			try {
////				La size remonte t'elle une erreur
//				int size = (Integer) getSize.invoke(o);
//			} catch (Exception esize) {
//				writeAMFNull();
//				return;
//			}
//		} catch (NoSuchMethodException ex) {
//		} catch (SecurityException ex) {
//		}
//		try {
////			L'object à t'il une method values
//			Method values = o.getClass().getMethod("values");
//			try {
////				values remonte t'il une erreur
//				Collection objs = (Collection) values.invoke(o);
//			} catch (Exception evalues) {
//				writeAMFNull();
//				return;
//			}
//		} catch (NoSuchMethodException ex) {
//		} catch (SecurityException ex) {
//		}

		if (o instanceof PropertyProxy) {
			proxy = (PropertyProxy) o;
			o = proxy.getDefaultInstance();

			// The proxy may wrap a null default instance, if so, short circuit here.
			if (o == null) {
				writeAMFNull();
				return;
			} // HACK: Short circuit and unwrap if PropertyProxy is wrapping an Array
			// or Collection or Map with legacyMap as true since we don't yet have
			// the ability to proxy multiple AMF types. We write an AMF Array directly
			// instead of an AMF Object...
			else if (o instanceof Collection) {
				if (context.legacyCollection) {
					writeCollection((Collection) o, proxy.getDescriptor());
				} else {
					writeArrayCollection((Collection) o, proxy.getDescriptor());
				}
				return;
			} else if (o.getClass().isArray()) {
				writeObjectArray((Object[]) o, proxy.getDescriptor());
				return;
			} else if (context.legacyMap && o instanceof Map && !(o instanceof ASObject)) {
				writeMapAsECMAArray((Map) o);
				return;
			}
		}

		out.write(kObjectType);

		if (!byReference(o)) {
			if (proxy == null) {
				proxy = PropertyProxyRegistry.getProxyAndRegister(o);
			}

			writePropertyProxy(proxy, o);
		}
	}

	/**
	 * @exclude
	 */
	protected void writePropertyProxy(PropertyProxy proxy, Object instance) throws IOException {
		Class cl = instance.getClass();
		/*
		 * At this point we substitute the instance we want to serialize.
		 */
		Object newInst = proxy.getInstanceToSerialize(instance);
		if (newInst != instance) {
			// We can't use writeAMFNull here I think since we already added this object
			// to the object table on the server side.  The player won't have any way
			// of knowing we have this reference mapped to null.
			if (newInst == null) {
				throw new MessageException("PropertyProxy.getInstanceToSerialize class: " + proxy.getClass() + " returned null for instance class: " + instance.getClass().getName());
			}

			// Grab a new proxy if necessary for the new instance
			proxy = PropertyProxyRegistry.getProxyAndRegister(newInst);
			instance = newInst;
		}

		List<String> propertyNames = null;
		boolean externalizable = proxy.isExternalizable(instance);

		if (!externalizable) {
			propertyNames = proxy.getPropertyNames(instance);
			// filter write-only properties
			if (proxy instanceof BeanProxy) {
				BeanProxy bp = (BeanProxy) proxy;
				if (propertyNames != null && !propertyNames.isEmpty()) {
					List<String> propertiesToRemove = new ArrayList<String>();
					for (String propName : propertyNames) {
						// Modif by hhf prise en compte des field dit FlexTransient
						if (!Map.class.isAssignableFrom(cl)) {
							Class tmpClass = cl;
							while (!tmpClass.equals(Object.class)) {
								try {
									Field f = tmpClass.getDeclaredField(propName);
									if ((f.isAnnotationPresent(FlexTransient.class))) {
										if (isDebug) {
											trace.writeString("Field " + propName + " annoted FlexTransient. It will be ignore.");
										}
										propertiesToRemove.add(propName);
									}
									break;
								} catch (NoSuchFieldException e) {
									tmpClass = tmpClass.getSuperclass();
								}
							}
							if (tmpClass.equals(Object.class)) {
								if (isDebug) {
									trace.writeString("Field " + propName + " not found on " + cl.getName() + " or parent. It will be ignore.");
								}
								propertiesToRemove.add(propName);
							}
						}
						if (bp.isWriteOnly(instance, propName)) {
							propertiesToRemove.add(propName);
						}
					}
					if (propertiesToRemove != null) {
						propertyNames.removeAll(propertiesToRemove);
					}
				}
			}
		}


		TraitsInfo ti = new TraitsInfo(proxy.getAlias(instance), proxy.isDynamic(), externalizable, propertyNames);
		writeObjectTraits(ti);

		if (externalizable) {
			// Call user defined serialization
			((Externalizable) instance).writeExternal(this);
		} else if (propertyNames != null && !propertyNames.isEmpty()) {
			for (int i = 0; i < propertyNames.size(); i++) {
				String propName = (String) propertyNames.get(i);
				Object value = proxy.getValue(instance, propName);
				writeObjectProperty(propName, value);
			}
		}

		writeObjectEnd();
	}

	/**
	 * Serialize an array of primitives.
	 * <p>
	 * Primitives include the following:
	 * boolean, char, double, float, long, int, short, byte
	 * </p>
	 *
	 * @param obj An array of primitives
	 * @exclude
	 */
	protected void writePrimitiveArray(Object obj) throws IOException {
		Class aType = obj.getClass().getComponentType();

		if (aType.equals(Character.TYPE)) {
			//Treat char[] as a String
			char[] c = (char[]) obj;
			writeCharArrayAsString(c);
		} else if (aType.equals(Byte.TYPE)) {
			writeAMFByteArray((byte[]) obj);
		} else {
			out.write(kArrayType);

			if (!byReference(obj)) {
				if (aType.equals(Boolean.TYPE)) {
					boolean[] b = (boolean[]) obj;

					// Write out an invalid reference, storing the length in the unused 28-bits.
					writeUInt29((b.length << 1) | 1);

					// Send an empty string to imply no named keys
					writeStringWithoutType(EMPTY_STRING);

					if (isDebug) {
						trace.startAMFArray(getObjectTableSize());

						for (int i = 0; i < b.length; i++) {
							trace.arrayElement(i);
							writeAMFBoolean(b[i]);
						}

						trace.endAMFArray();
					} else {
						for (int i = 0; i < b.length; i++) {
							writeAMFBoolean(b[i]);
						}
					}
				} else if (aType.equals(Integer.TYPE) || aType.equals(Short.TYPE)) {
					//We have a primitive number, either an int or short
					//We write all of these as Integers...
					int length = Array.getLength(obj);

					// Write out an invalid reference, storing the length in the unused 28-bits.
					writeUInt29((length << 1) | 1);
					// Send an empty string to imply no named keys
					writeStringWithoutType(EMPTY_STRING);

					if (isDebug) {
						trace.startAMFArray(getObjectTableSize());

						for (int i = 0; i < length; i++) {
							trace.arrayElement(i);
							int v = Array.getInt(obj, i);
							writeAMFInt(v);
						}

						trace.endAMFArray();
					} else {
						for (int i = 0; i < length; i++) {
							int v = Array.getInt(obj, i);
							writeAMFInt(v);
						}
					}
				} else {
					//We have a primitive number, either a double, float, or long
					//We write all of these as doubles...
					int length = Array.getLength(obj);

					// Write out an invalid reference, storing the length in the unused 28-bits.
					writeUInt29((length << 1) | 1);
					// Send an empty string to imply no named keys
					writeStringWithoutType(EMPTY_STRING);

					if (isDebug) {
						trace.startAMFArray(getObjectTableSize());

						for (int i = 0; i < length; i++) {
							trace.arrayElement(i);
							double v = Array.getDouble(obj, i);
							writeAMFDouble(v);
						}

						trace.endAMFArray();
					} else {
						for (int i = 0; i < length; i++) {
							double v = Array.getDouble(obj, i);
							writeAMFDouble(v);
						}
					}
				}
			}
		}
	}

	/**
	 * @exclude
	 */
	protected void writeAMFByteArray(byte[] ba) throws IOException {
		out.write(kByteArrayType);

		if (!byReference(ba)) {
			int length = ba.length;

			// Write out an invalid reference, storing the length in the unused 28-bits.
			writeUInt29((length << 1) | 1);

			if (isDebug) {
				trace.startByteArray(getObjectTableSize(), length);
			}

			out.write(ba, 0, length);
		}
	}

	/**
	 * @exclude
	 */
	protected void writeAMFByteArray(Byte[] ba) throws IOException {
		out.write(kByteArrayType);

		if (!byReference(ba)) {
			int length = ba.length;

			// Write out an invalid reference, storing the length in the unused 28-bits.
			writeUInt29((length << 1) | 1);

			if (isDebug) {
				trace.startByteArray(getObjectTableSize(), length);
			}

			for (int i = 0; i < ba.length; i++) {
				Byte b = ba[i];
				if (b == null) {
					out.write(0);
				} else {
					out.write(b.byteValue());
				}
			}
		}
	}

	/**
	 * @exclude
	 */
	protected void writeCharArrayAsString(Character[] ca) throws IOException {
		int length = ca.length;
		char[] chars = new char[length];

		for (int i = 0; i < length; i++) {
			Character c = ca[i];
			if (c == null) {
				chars[i] = 0;
			} else {
				chars[i] = ca[i].charValue();
			}
		}
		writeCharArrayAsString(chars);
	}

	/**
	 * @exclude
	 */
	protected void writeCharArrayAsString(char[] ca) throws IOException {
		String str = new String(ca);
		writeAMFString(str);
	}

	/**
	 * @exclude
	 */
	protected void writeObjectArray(Object[] values, SerializationDescriptor descriptor) throws IOException {
		out.write(kArrayType);

		if (!byReference(values)) {
			if (isDebug) {
				trace.startAMFArray(getObjectTableSize());
			}

			writeUInt29((values.length << 1) | 1);

			// Send an empty string to imply no named keys
			writeStringWithoutType(EMPTY_STRING);

			for (int i = 0; i < values.length; ++i) {
				if (isDebug) {
					trace.arrayElement(i);
				}

				Object item = values[i];
				if (item != null && descriptor != null && !(item instanceof String)
						  && !(item instanceof Number) && !(item instanceof Boolean)
						  && !(item instanceof Character)) {
					PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
					proxy = (PropertyProxy) proxy.clone();
					proxy.setDescriptor(descriptor);
					proxy.setDefaultInstance(item);
					item = proxy;
				}
				writeObject(item);
			}

			if (isDebug) {
				trace.endAMFArray();
			}
		}
	}

	/**
	 * @exclude
	 */
	protected void writeCollection(Collection c, SerializationDescriptor descriptor) throws IOException {
		// résoud le probleme de collection lazy loading by hhf
		if(!isInstantiated(c)) {
			writeAMFNull();
			return;
		}
//		try { 
//			c.size();
//		} catch (Throwable e) {
//			writeAMFNull();
//			return;
//		}
		out.write(kArrayType);

		// Note: We process Collections independently of Object[]
		// as we want the reference to be based on the actual
		// Collection.
		if (!byReference(c)) {
			if (isDebug) {
				trace.startAMFArray(getObjectTableSize());
			}

			writeUInt29((c.size() << 1) | 1);

			// Send an empty string to imply no named keys
			writeStringWithoutType(EMPTY_STRING);

			Iterator it = c.iterator();
			int i = 0;
			while (it.hasNext()) {
				if (isDebug) {
					trace.arrayElement(i);
				}

				Object item = it.next();

				if (item != null && descriptor != null && !(item instanceof String)
						  && !(item instanceof Number) && !(item instanceof Boolean)
						  && !(item instanceof Character)) {
					PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
					proxy = (PropertyProxy) proxy.clone();
					proxy.setDescriptor(descriptor);
					proxy.setDefaultInstance(item);
					item = proxy;
				}
				writeObject(item);

				i++;
			}

			if (isDebug) {
				trace.endAMFArray();
			}
		}
	}

	/**
	 * @exclude
	 */
	protected void writeUInt29(int ref) throws IOException {
		// Represent smaller integers with fewer bytes using the most
		// significant bit of each byte. The worst case uses 32-bits
		// to represent a 29-bit number, which is what we would have
		// done with no compression.

		// 0x00000000 - 0x0000007F : 0xxxxxxx
		// 0x00000080 - 0x00003FFF : 1xxxxxxx 0xxxxxxx
		// 0x00004000 - 0x001FFFFF : 1xxxxxxx 1xxxxxxx 0xxxxxxx
		// 0x00200000 - 0x3FFFFFFF : 1xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx
		// 0x40000000 - 0xFFFFFFFF : throw range exception
		if (ref < 0x80) {
			// 0x00000000 - 0x0000007F : 0xxxxxxx
			out.writeByte(ref);
		} else if (ref < 0x4000) {
			// 0x00000080 - 0x00003FFF : 1xxxxxxx 0xxxxxxx
			out.writeByte(((ref >> 7) & 0x7F) | 0x80);
			out.writeByte(ref & 0x7F);

		} else if (ref < 0x200000) {
			// 0x00004000 - 0x001FFFFF : 1xxxxxxx 1xxxxxxx 0xxxxxxx
			out.writeByte(((ref >> 14) & 0x7F) | 0x80);
			out.writeByte(((ref >> 7) & 0x7F) | 0x80);
			out.writeByte(ref & 0x7F);

		} else if (ref < 0x40000000) {
			// 0x00200000 - 0x3FFFFFFF : 1xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx
			out.writeByte(((ref >> 22) & 0x7F) | 0x80);
			out.writeByte(((ref >> 15) & 0x7F) | 0x80);
			out.writeByte(((ref >> 8) & 0x7F) | 0x80);
			out.writeByte(ref & 0xFF);

		} else {
			// 0x40000000 - 0xFFFFFFFF : throw range exception
			throw new MessageException("Integer out of range: " + ref);
		}
	}

	/**
	 * @exclude
	 */
	public void writeAMFUTF(String s) throws IOException {
		int strlen = s.length();
		int utflen = 0;
		int c, count = 0;

		char[] charr = getTempCharArray(strlen);
		s.getChars(0, strlen, charr, 0);

		for (int i = 0; i < strlen; i++) {
			c = charr[i];
			if (c <= 0x007F) {
				utflen++;
			} else if (c > 0x07FF) {
				utflen += 3;
			} else {
				utflen += 2;
			}
		}

		writeUInt29((utflen << 1) | 1);

		byte[] bytearr = getTempByteArray(utflen);

		for (int i = 0; i < strlen; i++) {
			c = charr[i];
			if (c <= 0x007F) {
				bytearr[count++] = (byte) c;
			} else if (c > 0x07FF) {
				bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
				bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
				bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
			} else {
				bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
				bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
			}
		}
		out.write(bytearr, 0, utflen);
	}

	/**
	 * Attempts to serialize the object as a reference.
	 * If the object cannot be serialized as a reference, it is stored
	 * in the reference collection for potential future encounter.
	 *
	 * @return Success/failure indicator as to whether the object could be
	 *         serialized as a reference.
	 * @exclude
	 */
	protected boolean byReference(Object o) throws IOException {
		if (objectTable != null && objectTable.containsKey(o)) {
			try {
				int refNum = objectTable.get(o).intValue();

				if (isDebug) {
					trace.writeRef(refNum);
				}

				writeUInt29(refNum << 1);

				return true;
			} catch (ClassCastException e) {
				throw new IOException("Object reference is not an Integer");
			}
		} else {
			if (objectTable == null) {
				objectTable = new IdentityHashMap<Object, Integer>(64);
			}
			objectTable.put(o, Integer.valueOf(objectTable.size()));
			return false;
		}
	}

	/**
	 * @exclude
	 */
	public void addObjectReference(Object o) throws IOException {
		byReference(o);
	}

	/**
	 * @exclude
	 */
	protected boolean byReference(String s) throws IOException {
		if (stringTable != null && stringTable.containsKey(s)) {
			try {
				int refNum = stringTable.get(s).intValue();

				writeUInt29(refNum << 1);

				if (isDebug && Trace.amf) {
					trace.writeStringRef(refNum);
				}
				return true;
			} catch (ClassCastException e) {
				throw new IOException("String reference is not an Integer");
			}
		} else {
			if (stringTable == null) {
				stringTable = new HashMap<String, Integer>(64);
			}
			stringTable.put(s, Integer.valueOf(stringTable.size()));
			return false;
		}
	}

	/**
	 * @exclude
	 */
	protected boolean byReference(TraitsInfo ti) throws IOException {
		if (traitsTable != null && traitsTable.containsKey(ti)) {
			try {
				int refNum = traitsTable.get(ti).intValue();

				writeUInt29((refNum << 2) | 1);

				if (isDebug && Trace.amf) {
					trace.writeTraitsInfoRef(refNum);
				}
				return true;
			} catch (ClassCastException e) {
				throw new IOException("TraitsInfo reference is not an Integer");
			}
		} else {
			if (traitsTable == null) {
				traitsTable = new HashMap<TraitsInfo, Integer>(10);
			}
			traitsTable.put(ti, Integer.valueOf(traitsTable.size()));
			return false;
		}
	}

	protected int getObjectTableSize() {
		return objectTable != null ? objectTable.size() - 1 : 0;
	}
}
